package ARTM

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"regexp"
	"strconv"
	"time"
)

var (
	ErrIdNoLength              = errors.New("string length is not 18")        //字符串长度不是18
	ErrIdNoInvalid             = errors.New("not a valid ID format")          //不是有效的身份证号格式
	ErrIdNoInvalidLocationCode = errors.New("not a valid location code")      //不是有效的地区代码
	ErrIdNoInvalidBirthday     = errors.New("not a valid birthday")           //不是有效的生日日期
	ErrIdNoDateAfterToday      = errors.New("birthday must before today")     //生日必须在今天之前
	ErrIdNoDateBefore1900      = errors.New("birthday must after 1900-01-01") //生日必须在1900-01-01之后
)

type IdCardInfo struct {
	Number   string `` //号码
	Gender   rune   `` //M 男，F 女
	Birthday string `` //生日
	State    string `` //省
	City     string `` //市
	Region   string `` //区县
}

//中国大陆18位身份证号验证
//本算法公验证身份证号是否符合规范，不具备联网验证功能
//v1.0.0 基本的身份证号校验
//v1.0.1 强化对字符串格式的校验，增加 地区码、生日的校验
func IsCMIdNoValid(idNumber string) (bool, *IdCardInfo, error) {

	//判断长度是否为18
	if len(idNumber) != 18 {
		return false, nil, ErrIdNoLength
	}

	idCardInfo := &IdCardInfo{}

	//校验字符串是否符合身份证号格式
	valid, err := idCardInfo.isCMIdNoFormatValid(idNumber)
	if err != nil {
		return false, nil, err
	}
	if !valid {
		return false, nil, ErrIdNoInvalid
	}

	//计算前17个号码的 积 和
	var (
		mult    = [17]int{7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2} //前17个号码依次相乘的数
		sum17No int                                                            //前17个号码相乘后的和
		idRune  = []rune(idNumber)                                             //将字符串拆分为字符
	)
	for i := 0; i < 17; i++ {
		//遍历身份证号码，求积与和
		toInt, err := strconv.Atoi(string(idRune[i])) //字符转int
		if err != nil {
			return false, nil, err
		}
		sum17No += toInt * mult[i] //乘数求和
	}

	//取余11
	remainder := sum17No % 11
	fmt.Println("取余：", remainder)

	//判断余数是否与身份证最后一位对应匹配
	matchNo := [11]rune{'1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'}

	////////虽然余数不可能出现大于11的情况，但这里是否有BUG？
	if idRune[17] == matchNo[remainder] || (idRune[17] == 'x' && matchNo[remainder] == 'X') {

		//将其它数据赋值给对象成员
		//提取性别码，并转为int
		g := idNumber[16:17]
		gint, err := strconv.Atoi(g)
		if err != nil {
			return false, nil, err
		}
		if gint%2 == 1 {
			idCardInfo.Gender = 'M'
		} else {
			idCardInfo.Gender = 'F'
		}

		//将号码复制到对象中
		idCardInfo.Number = idNumber

		return true, idCardInfo, nil
	}

	return false, nil, nil
}

//校验字符串是否符合身份证号格式
func (i *IdCardInfo) isCMIdNoFormatValid(idNumber string) (bool, error) {
	//v1.0.0
	//判断前17位是数字，以及最后一位是数字或者X或者x
	matched, err := regexp.MatchString(`^\d{17}(\d|X|x)$`, idNumber)
	if err != nil {
		return false, err
	}
	if !matched {
		return false, ErrIdNoInvalid
	}

	//v1.0.1
	//-------------------
	// 判断地区码是否有效
	//-------------------

	//从文件读取地区码数据
	bytes, err := ioutil.ReadFile("local.json")

	type Region struct {
		Name string `json:"name"`
		Code string `json:"code"`
	}
	type City struct {
		Name    string   `json:"name"`
		Code    string   `json:"code"`
		Regions []Region `json:"regions"`
	}
	type State struct {
		Name   string `json:"name"`
		Code   string `json:"code"`
		Cities []City `json:"cities"`
	}
	type Country struct {
		States []State `json:"states"`
	}

	//将json转为对象
	var country Country
	err = json.Unmarshal(bytes, &country)
	if err != nil {
		return false, err
	}

	//就目前为止，短期内，省、地级市代码不会变，只要省、地级市代码不在对象数据中，因此判定 身份证号无效
	//区、县级市好像每隔几年就会变的，这里就不做判断了，只作取区、县级市名称用
	var matchCity bool //匹配到市

	for _, state := range country.States {
		if state.Code == idNumber[0:2] {
			//如果匹配到省级代码，把名称赋值给idLocalState，再继续遍历其下级地区
			i.State = state.Name
			for _, city := range state.Cities {
				if city.Code == idNumber[2:4] {
					//如果匹配到地级市代码，把名称赋值给idLocalCity，再继续遍历其下级地区
					matchCity = true
					i.City = city.Name
					for _, region := range city.Regions {
						if region.Code == idNumber[4:6] {
							//如果匹配到县级市代码，把名称赋值给idLocalRegion
							i.Region = region.Name
							break
						}
					}
					break
				}
			}
			break
		}
	}
	//判断是否已匹配到省、市，只要匹配到市，省肯定能匹配到
	if !matchCity {
		return false, ErrIdNoInvalidLocationCode
	}

	//-------------------
	// 判断生日是否有效
	//-------------------
	//生日不能小于1900 大于今天

	//提取生日
	date, err := time.ParseInLocation("20060102", idNumber[6:14], time.Local)
	if err != nil {
		return false, ErrIdNoInvalidBirthday
	}

	//日期在今天之前
	if !date.Before(time.Now()) {
		return false, ErrIdNoDateAfterToday
	}
	//日期在1900-01-01之后
	if !date.After(time.Date(1900, 1, 1, 0, 0, 0, 0, time.Local)) {
		return false, ErrIdNoDateBefore1900
	}

	i.Birthday = date.Format("2006-01-02")

	return true, nil
}

/*
计算方法：

第一步：
将身份证号码的第1位数字与7相乘；
将身份证号码的第2位数字与9相乘；
将身份证号码的第3位数字与10相乘；
将身份证号码的第4位数字与5相乘；
将身份证号码的第5位数字与8相乘；
将身份证号码的第6位数字与4相乘；
将身份证号码的第7位数字与2相乘；
将身份证号码的第8位数字与1相乘；
将身份证号码的第9位数字与6相乘；
将身份证号码的第10位数字与3相乘；
将身份证号码的第11位数字与7相乘；
将身份证号码的第12位数字与9相乘；
将身份证号码的第13位数字与10相乘；
将身份证号码的第14位数字与5相乘；
将身份证号码的第15位数字与8相乘；
将身份证号码的第16位数字与4相乘；
将身份证号码的第17位数字与2相乘。

第二步：
将第一步身份证号码1~17位相乘的结果求和，全部加起来。

第三步：
用第二步计算出来的结果除以11，
这样就会出现余数为0，1，2，3，4，5，6，7，8，9，10共11种可能性。

第四步：
如果余数为0，那对应的最后一位身份证的号码为1；
如果余数为1，那对应的最后一位身份证的号码为0；
如果余数为2，那对应的最后一位身份证的号码为X；
如果余数为3，那对应的最后一位身份证的号码为9；
如果余数为4，那对应的最后一位身份证的号码为8；
如果余数为5，那对应的最后一位身份证的号码为7；
如果余数为6，那对应的最后一位身份证的号码为6；
如果余数为7，那对应的最后一位身份证的号码为5；
如果余数为8，那对应的最后一位身份证的号码为4；
如果余数为9，那对应的最后一位身份证的号码为3；
如果余数为10，那对应的最后一位身份证的号码为2。

====================================
手动尝试计算
====================================

以号码（11000019991122333?）为例，该号码为验证算法使用，如果号码与你的号码相同，纯属巧合！

第几位数		号码		乘数		乘积
第1位数		1		7		7
第2位数		1		9		9
第3位数		0		10		0
第4位数		0		5		0
第5位数		0		8		0
第6位数		0		4		0
第7位数		1		2		2
第8位数		9		1		9
第9位数		9		6		54
第10位数		9		3		27
第11位数		1		7		7
第12位数		1		9		9
第13位数		2		10		20
第14位数		2		5		10
第15位数		3		8		24
第16位数		3		4		12
第17位数		3		2		6
第18位数		?

1~17位数的乘积 和为：			196
乘积和取余11后为：				9	对应的数字为3

因此该身份证号码最后一位是：3

*/
